/*
 *  Copyright 2010 Andrew Kroh
 *
 *  Kroh-IO is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Kroh-IO is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package kroh.io;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.util.TooManyListenersException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Serial port implementation that makes native calls to
 * the underlying OS.
 */
public final class KrohSerialPort extends SerialPort
{
    private static final Logger logger = LoggerFactory.getLogger(KrohSerialPort.class);
    
    /**
     * Name of the native library to load.
     */
    private static final String LIBRARY_NAME = "krohio";
    
    private static final int DEBUG_OFF = 0;
    private static final int DEBUG_ERROR = 2;
    private static final int DEBUG_WARN = 4;
    private static final int DEBUG_INFO = 5;
    private static final int DEBUG_DEBUG = 6;
    private static final int DEBUG_TRACE = 8;
    
    /*
     * Bit masks are used to describe what events the
     * <code>SerialPortEventListener</code> has subscribed for. The bit masks
     * are also used to communicate which events have occurred from the native
     * code to the Java code through a single integer.
     */
    private static final int NOTIFY_DATA_AVAILABLE = 1;
    private static final int NOTIFY_OUTPUT_EMPTY = 2;
    private static final int NOTIFY_CTS = 4;
    private static final int NOTIFY_DSR = 8;
    private static final int NOTIFY_RING_INDICATOR = 16;
    private static final int NOTIFY_CARRIER_DETECT = 32;
    private static final int NOTIFY_OVERRUN_ERROR = 64;
    private static final int NOTIFY_PARITIY_ERROR = 128;
    private static final int NOTIFY_FRAMING_ERROR = 256;
    private static final int NOTIFY_BREAK_INTERRUPT = 512;
    
    /**
     * File descriptor for the opened port.
     */
    private int portFileDescriptor;
    
    /**
     * Baud rate of the serial port.
     */
    private int baudRate = 9600;
    
    /** 
     * Data bits port parameter.
     */
    private int dataBits = DATABITS_8;
    
    /** 
     * Stop bits port parameter.
     */
    private int stopBits = STOPBITS_1;
    
    /** 
     * Parity port parameter.
     */
    private int parity = PARITY_NONE;
    
    /** 
     * Flow control mode. 
     * */
    private int flowControl = FLOWCONTROL_NONE;
    
    /**
     * Output buffer size.
     */
    private int outputBufferSize;
    
    /** 
     * Output stream 
     */
    private final SerialOutputStream serialOutputStream = new SerialOutputStream();

    /**
     * Input buffer size.
     */
    private int inputBufferSize;
    
    /** 
     * Input stream 
     */
    private final SerialInputStream serialInputStream = new SerialInputStream();
    
    /** 
     * Receive timeout value. -1 indicates disabled.
     */
    private int receiveTimeout;
    
    /** 
     * Receive threshold value. -1 indicates disabled.
     */
    private int receiveThreshold;
    
    /** 
     * Serial port event listener. There is only one listener allowed
     * per port.
     */
    private SerialPortEventListener serialPortEventListener;
    
    /**
     * Bit mask used to described the type of events that the
     * <code>SerialPortEventListener</code> subscribed to.
     */
    private int listenerNotificationBitMask;
    
    // ----------------------------------------------------------------
    
    static 
    {
        try
        {
            NativeLibLoader.loadLibrary(LIBRARY_NAME);
            
            logger.trace("Loaded native library <" + LIBRARY_NAME + ">.");
            
            initDebug();
        }
        catch (UnsatisfiedLinkError e)
        {
            logger.error("Unable to load native library <" + LIBRARY_NAME + ">.", e);
        }
    }
    
    private static void initDebug()
    {
        if (logger.isTraceEnabled())
        {
            nativeSetDebugLevel(DEBUG_TRACE);
        }
        else if (logger.isDebugEnabled())
        {
            nativeSetDebugLevel(DEBUG_DEBUG);
        }
        else if (logger.isInfoEnabled())
        {
            nativeSetDebugLevel(DEBUG_INFO);
        }
        else if (logger.isWarnEnabled())
        {
            nativeSetDebugLevel(DEBUG_WARN);
        }
        else if (logger.isErrorEnabled())
        {
            nativeSetDebugLevel(DEBUG_ERROR);
        }
        else
        {
            nativeSetDebugLevel(DEBUG_OFF);
        }
    }
    
    // ----------------------------------------------------------------
    
    /**
     * Constructor. Opens the named serial port.
     * 
     * @param portName
     *            name of the device to open
     * 
     * @throws PortInUseException
     *             if the port is in use by another user
     */
    public KrohSerialPort(String portName) throws PortInUseException
    {
        this.name = portName;

        logger.trace("Opening native port <" + portName + ".>");
        
        portFileDescriptor = nativeOpen(portName);
        
        logger.trace("Native port opened. File Descriptor = <" + portFileDescriptor + ">.");
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#getBaudRate()
     */
    public int getBaudRate()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeGetBaudRate(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#getDataBits()
     */
    public int getDataBits()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeGetDataBits(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#getStopBits()
     */
    public int getStopBits()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeGetStopBits(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#getParity()
     */
    public int getParity()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeGetParity(portFileDescriptor);
    }

    /* 
     * Note that the duration parameter is interpreted
     * differently by some operating systems and ignored
     * by some; therefore, no units are associated with
     * it. It is recommended to you use 0.
     * 
     * (non-Javadoc)
     * @see gnu.io.SerialPort#sendBreak(int)
     */
    public void sendBreak(int duration)
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        nativeSendBreak(portFileDescriptor, duration);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#setFlowControlMode(int)
     */
    public void setFlowControlMode(int flowControl)
        throws UnsupportedCommOperationException
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        // Do input parameter checking:
        if ((flowControl & FLOWCONTROL_RTSCTS_IN) != 0)
        {
            if (((flowControl & FLOWCONTROL_XONXOFF_IN) != 0) || 
                    ((flowControl & FLOWCONTROL_XONXOFF_OUT) != 0))
            {
                throw new UnsupportedCommOperationException(
                        "Cannot mix hardware and software flow control.");
            }
            else if (((flowControl & FLOWCONTROL_RTSCTS_OUT) != 0)
                    && ((((flowControl & FLOWCONTROL_XONXOFF_IN) != 0) || 
                            ((flowControl & FLOWCONTROL_XONXOFF_OUT) != 0))))
            {
                throw new UnsupportedCommOperationException(
                        "Cannot mix hardware and software flow control.");
            }
        }

        nativeSetSerialPortParams(portFileDescriptor, baudRate, dataBits,
                                  stopBits, parity, flowControl);
        this.flowControl = flowControl;
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#getFlowControlMode()
     */
    public int getFlowControlMode()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }

        return nativeGetFlowControlMode(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#setSerialPortParams(int, int, int, int)
     */
    public void setSerialPortParams(int baud, int dataBits, int stopBits, int parity)
            throws UnsupportedCommOperationException
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        if (dataBits != DATABITS_5 && dataBits != DATABITS_6 &&
                dataBits != DATABITS_7 && dataBits != DATABITS_8)
        {
            throw new UnsupportedCommOperationException(
                    "Unsupported number of databits <" + dataBits + ">.");
        }

        if (stopBits != STOPBITS_1 && stopBits != STOPBITS_2 &&
                stopBits != STOPBITS_1_5)
        {
            throw new UnsupportedCommOperationException(
                    "Unsupported number of stopbits <" + stopBits + ">.");
        }

        if (parity != PARITY_NONE && parity != PARITY_ODD &&
                parity != PARITY_EVEN && parity != PARITY_MARK &&
                parity != PARITY_SPACE)
        {
            throw new UnsupportedCommOperationException(
                    "Unsupported parity value <" + parity + ">.");
        }

        nativeSetSerialPortParams(portFileDescriptor, baud, dataBits,
                                  stopBits, parity, flowControl);

        this.baudRate = baud;
        this.dataBits = dataBits;
        this.stopBits = stopBits;
        this.parity = parity;
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#setDTR(boolean)
     */
    public void setDTR(boolean dtr)
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        nativeSetDTR(portFileDescriptor, dtr);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#isDTR()
     */
    public boolean isDTR()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeIsDTR(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#setRTS(boolean)
     */
    public void setRTS(boolean rts)
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        nativeSetRTS(portFileDescriptor, rts);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#isRTS()
     */
    public boolean isRTS()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeIsRTS(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#isCTS()
     */
    public boolean isCTS()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeIsCTS(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#isDSR()
     */
    public boolean isDSR()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeIsDSR(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#isRI()
     */
    public boolean isRI()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeIsRI(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#isCD()
     */
    public boolean isCD()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        return nativeIsCD(portFileDescriptor);
    }
    
    /* (non-Javadoc)
     * @see gnu.io.SerialPort#addEventListener(gnu.io.SerialPortEventListener)
     */
    public void addEventListener(SerialPortEventListener listener)
            throws TooManyListenersException
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        if (serialPortEventListener != null)
        {
            throw new TooManyListenersException();
        }
        
        serialPortEventListener = listener;
        
        // TODO: make sure that a port status checker thread is started
        //       to perform notifications
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#removeEventListener()
     */
    public void removeEventListener()
    {
        serialPortEventListener = null;
        
        // TODO: stop the port status checker thread
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#getInputStream()
     */
    public InputStream getInputStream()
    {
        return serialInputStream;
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#getOutputStream()
     */
    public OutputStream getOutputStream()
    {
        return serialOutputStream;
    }
    
    // -----------------------------------------------------
    //      Receive Framing Methods (Unsupported)
    // -----------------------------------------------------

    /* (non-Javadoc)
     * @see gnu.io.CommPort#enableReceiveFraming(int)
     */
    public void enableReceiveFraming(int framingByte)
            throws UnsupportedCommOperationException
    {
        throw new UnsupportedCommOperationException();
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#disableReceiveFraming()
     */
    public void disableReceiveFraming()
    {
        // Unsupported
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#isReceiveFramingEnabled()
     */
    public boolean isReceiveFramingEnabled()
    {
        // Unsupported 
        return false;
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#getReceiveFramingByte()
     */
    public int getReceiveFramingByte()
    {
        // Unsupported 
        return 0;
    }
    
    // -----------------------------------------------------
    //      Receive Timeout Methods
    // -----------------------------------------------------

    /* (non-Javadoc)
     * @see gnu.io.CommPort#disableReceiveTimeout()
     */
    public void disableReceiveTimeout()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        receiveTimeout = -1;
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#enableReceiveTimeout(int)
     */
    public void enableReceiveTimeout(int timeout)
        throws UnsupportedCommOperationException
    {
        if (portFileDescriptor != 0)
        {
            if (timeout >= 0)
            {
                receiveTimeout = timeout;
            }
            else
            {
                throw new UnsupportedCommOperationException("Timeout value must be non-negative.");
            }
        }
        
        throw new IllegalStateException("Serial port is closed.");
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#isReceiveTimeoutEnabled()
     */
    public boolean isReceiveTimeoutEnabled()
    {
        return receiveTimeout > 0;
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#getReceiveTimeout()
     */
    public int getReceiveTimeout()
    {
        return receiveTimeout;
    }

    // -----------------------------------------------------
    //      Receive Threshold Methods
    // -----------------------------------------------------
    
    /* (non-Javadoc)
     * @see gnu.io.CommPort#enableReceiveThreshold(int)
     */
    public void enableReceiveThreshold(int threshold)
        throws UnsupportedCommOperationException
    {
        if (portFileDescriptor != 0)
        {
            if (threshold >= 0)
            {
                receiveThreshold = threshold;
            }
            else
            {
                throw new UnsupportedCommOperationException("Threshold must be non-negative.");
            }
        }
        
        throw new IllegalStateException("Serial port is closed.");
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#disableReceiveThreshold()
     */
    public void disableReceiveThreshold()
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        receiveThreshold = -1;
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#getReceiveThreshold()
     */
    public int getReceiveThreshold()
    {
        return receiveThreshold;
    }
    
    /* (non-Javadoc)
     * @see gnu.io.CommPort#isReceiveThresholdEnabled()
     */
    public boolean isReceiveThresholdEnabled()
    {
        return receiveThreshold >= 0;
    }
    
    // -----------------------------------------------------
    //      Buffer Size Methods
    // -----------------------------------------------------

    /* (non-Javadoc)
     * @see gnu.io.CommPort#setInputBufferSize(int)
     */
    public void setInputBufferSize(int size)
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        if (size < 0)
        {
            throw new IllegalArgumentException("Buffer size must be positive.");
        }

        inputBufferSize = size;
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#getInputBufferSize()
     */
    public int getInputBufferSize()
    {
        return inputBufferSize;
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#setOutputBufferSize(int)
     */
    public void setOutputBufferSize(int size)
    {
        if (portFileDescriptor == 0)
        {
            throw new IllegalStateException("Serial port is closed.");
        }
        
        if (size < 0)
        {
            throw new IllegalArgumentException("Buffer size must be positive.");
        }
        
        outputBufferSize = size;
    }

    /* (non-Javadoc)
     * @see gnu.io.CommPort#getOutputBufferSize()
     */
    public int getOutputBufferSize()
    {
        return outputBufferSize;
    }

    // -----------------------------------------------------
    //                Notify On --- Methods
    // -----------------------------------------------------

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnDataAvailable(boolean)
     */
    public void notifyOnDataAvailable(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_DATA_AVAILABLE;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_DATA_AVAILABLE);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnOutputEmpty(boolean)
     */
    public void notifyOnOutputEmpty(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_OUTPUT_EMPTY;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_OUTPUT_EMPTY);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnCTS(boolean)
     */
    public void notifyOnCTS(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_CTS;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_CTS);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnDSR(boolean)
     */
    public void notifyOnDSR(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_DSR;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_DSR);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnRingIndicator(boolean)
     */
    public void notifyOnRingIndicator(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_RING_INDICATOR;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_RING_INDICATOR);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnCarrierDetect(boolean)
     */
    public void notifyOnCarrierDetect(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_CARRIER_DETECT;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_CARRIER_DETECT);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnOverrunError(boolean)
     */
    public void notifyOnOverrunError(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_OVERRUN_ERROR;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_OVERRUN_ERROR);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnParityError(boolean)
     */
    public void notifyOnParityError(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_PARITIY_ERROR;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_PARITIY_ERROR);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnFramingError(boolean)
     */
    public void notifyOnFramingError(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_FRAMING_ERROR;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_FRAMING_ERROR);
        }
    }

    /* (non-Javadoc)
     * @see gnu.io.SerialPort#notifyOnBreakInterrupt(boolean)
     */
    public void notifyOnBreakInterrupt(boolean enable)
    {
        if (enable)
        {
            listenerNotificationBitMask &= NOTIFY_BREAK_INTERRUPT;
        }
        else
        {
            listenerNotificationBitMask &= ~(1 << NOTIFY_BREAK_INTERRUPT);
        }
    }
    
    /* (non-Javadoc)
     * @see gnu.io.CommPort#close()
     */
    public void close()
    {
        if (portFileDescriptor != 0)
        {
            logger.debug("Closing serial port <" + getName() + 
                    "> with fd = <" + portFileDescriptor + ">.");
            
            nativeClose(portFileDescriptor);
            portFileDescriptor = 0;
        }
        
        // Must call close on the super class to make sure
        // that the port is properly marked as close in the
        // CommPortIdentifier:
        super.close();
    }
    
    // ------------------------------------------------------------------
    //                      End SerialPort Methods
    // ------------------------------------------------------------------

    /* (non-Javadoc)
     * @see java.lang.Object#finalize()
     */
    protected void finalize()
    {
        close();
    }
    
    /**
     * OutputStream that writes to the serial port.
     */
    private class SerialOutputStream extends OutputStream
    {
        private boolean isClosed = false;
        
        public void close()
        {
            isClosed = true;
        }
        
        public void write(int data) throws IOException
        {
            write(new byte[]{(byte)data}, 0, 1);
        }

        public void write(byte data[]) throws IOException
        {
            write(data, 0, data.length);
        }

        public void write(byte data[], int offset, int length) throws IOException
        {
            if (portFileDescriptor == 0)
            {
                throw new IOException("SerialPort is closed.");
            }
            
            if (isClosed)
            {
                throw new IOException("OutputStream is closed.");
            }
            
            // Do parameter checking before calling native write:
            if (data == null)
            {
                throw new NullPointerException("Data array cannot be null.");
            }
            
            if (length + offset > data.length)
            {
                throw new IndexOutOfBoundsException(
                        "Offset + length is greater than array length.");
            }
            
            int rtn = nativeWrite(portFileDescriptor, data, offset, length);
            
            logger.debug("nativeWrite(fd=" + portFileDescriptor + 
                    ", array length=" + data.length + 
                    ", offset=" + offset + ", length=" + length + 
                    ") returned <" + rtn + ">");
        }

        public void flush() throws IOException
        {
            if (portFileDescriptor == 0)
            {
                throw new IOException("SerialPort is closed.");
            }
            
            if (isClosed)
            {
                throw new IOException("OutputStream is closed.");
            }
            
            int rtn = nativeFlush(portFileDescriptor);
            
            logger.debug("nativeFlush(fd=" + portFileDescriptor + 
                    ") returned <" + rtn + ">");
        }
    }

    /**
     * InputStream for reading data from the serial port.
     */
    private class SerialInputStream extends InputStream
    {
        boolean isClosed = false;
        
        public void close()
        {
            isClosed = true;
        }
        
        public int read() throws IOException
        {
            byte[] dest = new byte[1];
            read(dest);
            
            return dest[1];
        }

        public synchronized int read(byte dest[]) throws IOException
        {
            return read(dest, 0, dest.length);
        }

        public int read(byte dest[], int offset, int length)
                throws IOException
        {
            if (portFileDescriptor == 0)
            {
                throw new IOException("Serial port is closed.");
            }
            
            if (isClosed)
            {
                throw new IOException("InputStream is closed.");
            }
            
            if (dest == null)
            {
                throw new NullPointerException("Destination array cannot be null.");
            }
            
            int numBytesToRead = length;
            if (offset + length > dest.length)
            {
                numBytesToRead = dest.length - offset;
            }
            
            int rtn = nativeRead(portFileDescriptor, dest, offset, numBytesToRead, receiveTimeout, receiveThreshold);
            
            logger.debug("nativeRead(fd=" + portFileDescriptor + ", array length=" + dest.length + 
                    ", offset=" + offset + ", numBytesToRead=" + numBytesToRead + 
                    ", receiveTimeout=" + receiveTimeout + ", receiveThreshold=" + receiveThreshold + 
                    ") returned <" + rtn + ">");
            
            return rtn;
        }

        public int available() throws IOException
        {
            if (portFileDescriptor == 0)
            {
                throw new IOException("Serial port is closed");
            }
            
            if (isClosed)
            {
                throw new IOException("InputStream is closed.");
            }
            
            int rtn = nativeAvailable(portFileDescriptor);
            
            logger.debug("nativeAvailable(fd=" + portFileDescriptor + ") returned <" + rtn + ">");
            
            return rtn;
        }
    }

    // -------------------------------------------------------------------
    //                          End of Comm API 
    //                     -------------------------
    //                        Begin Native Methods
    // -------------------------------------------------------------------
    
    private static native void nativeSetDebugLevel(int debugLevel);
    
    /**
     * Native method that tests the serial port by opening it
     * and performing a test read.
     * 
     * @param device
     *            name of the device to test (like COM1 or /dev/ttyS0)
     * @param portType
     *            type of port to test, always
     *            <code>CommPortIdentifier.PORT_SERIAL</code> for this
     *            implementation
     * @return true if the port is valid, false otherwise
     */
    public static native boolean nativeTestSerialPort(String device);
    
    /**
     * Opens the specified port and returns a file descriptor to the opened
     * port.
     * 
     * @param name
     *            name of the port to open (like COM2 or /dev/ttyS0)
     *            
     * @return file descriptor to the port if opened successfully, zero
     *         otherwise
     *         
     * @throws PortInUseException
     *             if the port is in use by another user
     */
    private native int nativeOpen(String name) throws PortInUseException;
    
    /**
     * Returns the baud rate that the port is configured to use.
     * 
     * @param fd
     *          file descriptor to the opened port
     *          
     * @return the baud rate to the opened port, -1 if there was a problem
     */
    private native int nativeGetBaudRate(int fd);

    /**
     * Gets the currently configured number of data bits for the serial port.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return integer that can be equal to DATABITS_5, DATABITS_6, DATABITS_7,
     *         or DATABITS_8
     */
    private native int nativeGetDataBits(int fd);

    /**
     * Gets the currently defined stop bits.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return integer that can be equal to STOPBITS_1, STOPBITS_2, or
     *         STOPBITS_1_5
     */
    private native int nativeGetStopBits(int fd);

    /**
     * Gets the currently configured parity setting.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return integer that can be equal to PARITY_NONE, PARITY_ODD,
     *         PARITY_EVEN, PARITY_MARK or PARITY_SPACE.
     */
    private native int nativeGetParity(int fd);

    /**
     * Sends a break. Note that it may not be
     * possible to time the duration of the break under certain operating
     * systems. Hence this parameter is advisory.
     * 
     * @param fd
     *            file descriptor to the opened port
     * @param duration
     *            duration of break to send (advisory only)
     *            
     * @return true if break was sent successfully, false otherwise
     */
    private native boolean nativeSendBreak(int fd, int duration);
    
    /**
     * Sets serial port parameters.
     * 
     * @param fd
     *            file descriptor to the opened port
     * @param baud
     *            If the baud rate passed in by the application is unsupported
     *            by the driver, the driver will throw an
     *            UnsupportedCommOperationException
     * @param dataBits
     *            DATABITS_5, DATABITS_6, DATABITS_7, or DATABITS_8
     * @param stopBits
     *            STOPBITS_1, STOPBITS_2, or STOPBITS_1_5
     * @param parity
     *            PARITY_NONE, PARITY_ODD, PARITY_EVEN, PARITY_MARK or
     *            PARITY_SPACE
     * @param flowControl
     *            an integer bitmask of FLOWCONTROL_NONE, FLOWCONTROL_RTSCTS_IN,
     *            FLOWCONTROL_RTSCTS_OUT, FLOWCONTROL_XONXOFF_IN, and
     *            FLOWCONTROL_XONXOFF_OUT
     * 
     * @return true if successful, false otherwise
     */
    private native boolean nativeSetSerialPortParams(int fd, int baud,
            int dataBits, int stopBits, int parity, int flowControl);

    /**
     * Sets the flow control mode.
     * 
     * @param fd
     *            file descriptor to the opened port
     *            
     * @return an integer bitmask of FLOWCONTROL_NONE, FLOWCONTROL_RTSCTS_IN,
     *         FLOWCONTROL_RTSCTS_OUT, FLOWCONTROL_XONXOFF_IN, and
     *         FLOWCONTROL_XONXOFF_OUT
     */
    private native int nativeGetFlowControlMode(int fd);

    /**
     * Sets or clears the DTR (Data Terminal Ready) bit in the UART, if
     * supported by the underlying implementation.
     * 
     * @param fd
     *            file descriptor to the opened port
     * @param state
     *            true to set DTR, false to clear DTR
     */
    private native void nativeSetDTR(int fd, boolean state);

    /**
     * Gets the state of the DTR (Data Terminal Ready) bit in the UART, if
     * supported by the underlying implementation.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return true if DTR, false otherwise
     */
    private native boolean nativeIsDTR(int fd);

    /**
     * Sets or clears the RTS (Request To Send) bit in the UART, if supported by
     * the underlying implementation.
     * 
     * @param fd
     *            file descriptor to the opened port
     * @param state
     *            true to set RTS, false to clear RTS
     */
    private native void nativeSetRTS(int fd, boolean state);

    /**
     * Gets the state of the RTS (Request To Send) bit in the UART, if supported
     * by the underlying implementation.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return true if RTS is set, false is RTS is clear
     */
    private native boolean nativeIsRTS(int fd);

    /**
     * Gets the state of the CTS (Clear To Send) bit in the UART, if supported
     * by the underlying implementation.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return true if CTS (clear to send), false otherwise
     */
    private native boolean nativeIsCTS(int fd);

    /**
     * Gets the state of the DSR (Data Set Ready) bit in the UART, if supported
     * by the underlying implementation.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return true if DSR is set, false otherwise
     */
    private native boolean nativeIsDSR(int fd);

    /**
     * Gets the state of the RI (Ring Indicator) bit in the UART, if supported
     * by the underlying implementation.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return true if ring indicator is set, false otherwise
     */
    private native boolean nativeIsRI(int fd);

    /**
     * Gets the state of the CD (Carrier Detect) bit in the UART, if supported
     * by the underlying implementation.
     * 
     * @param fd
     *            file descriptor to the opened port
     * 
     * @return true if carrier detect is set, false otherwise
     */
    private native boolean nativeIsCD(int fd);
    
    /**
     * Closes the underlying serial port.
     * 
     * @param fd
     *          file descriptor to the opened port
     */
    private native boolean nativeClose(int fd);

    /**
     * Gets the number of bytes available to read.
     * 
     * @param fd
     *          file descriptor to the opened port
     *          
     * @return number of bytes to read
     * 
     * @throws IOException
     */
    private native int nativeAvailable(int fd) throws IOException;
    
    /**
     * Reads bytes from the serial port.
     * 
     * @param fd
     *            file descriptor to the opened port
     * @param destArray
     *            array to write the incoming data to
     * @param offset
     *            offset to begin writing at in the array
     * @param numBytesToRead
     *            number of bytes to read
     * @param receiveTimeoutMs
     *            receive timeout value is milliseconds
     * @param receiveThreshold
     *            read should return if this number of bytes are in the input
     *            buffer
     * 
     * @return number of bytes that were read
     * 
     * @throws IOException
     */
    private native int nativeRead(int fd, byte destArray[], int offset, int numBytesToRead, 
                                 int receiveTimeoutMs, int receiveThreshold)
            throws IOException;
    
    /**
     * @param fd
     *          file descriptor to the opened port
     * @param sourceData
     *          array of source bytes
     * @param offset
     *          offset into the source array where to begin reading
     * @param numBytesToWrite
     *          number of bytes to read from the source array
     *          
     * @return number of bytes wrote to the serial port
     * 
     * @throws IOException
     */
    private native int nativeWrite(int fd, byte sourceData[], int offset, int numBytesToWrite)
            throws IOException;
    
    /**
     * Flushes all bytes from the output buffer.
     * 
     * @param fd
     *          file descriptor to the opened port
     *          
     * @return number of bytes flushed from the output buffer
     */
    private native int nativeFlush(int fd);
}
